Перейти к основному содержимому

5.14. Жизненный цикл Swift-приложения

Разработчику Архитектору

Создание проекта и структура приложения

Инициализация проекта в Xcode

Создание нового проекта на Swift начинается с запуска Xcode и выбора опции "Create a new Xcode project". Xcode предоставляет различные шаблоны приложений: App, Game, Augmented Reality App, Document App, Framework, Package. Для стандартного приложения используется шаблон "App".

При создании проекта указываются следующие параметры:

  • Название продукта
  • Команда разработки
  • Идентификатор организации
  • Интерфейс (Storyboard или SwiftUI)
  • Язык программирования (Swift или Objective-C)
  • Поддержка Core Data
  • Включение тестирования

Пример конфигурации нового проекта:

// Package.swift для Swift Package Manager
let package = Package(
name: "MyApplication",
platforms: [
.iOS(.v15)
],
products: [
.library(
name: "MyApplication",
targets: ["MyApplication"]
)
],
dependencies: [],
targets: [
.target(
name: "MyApplication",
dependencies: []
)
]
)

Структура каталогов проекта

Стандартная структура проекта Xcode включает следующие основные каталоги:

MyApplication/
├── MyApplication/
│ ├── Assets.xcassets/
│ ├── Base.lproj/
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ └── ViewController.swift
├── MyApplication.xcodeproj/
├── MyApplicationTests/
└── MyApplicationUITests/

Каталог Assets.xcassets содержит все изображения, цвета, шрифты и другие ресурсы приложения. Base.lproj содержит файлы интерфейса. AppDelegate.swift и SceneDelegate.swift управляют жизненным циклом приложения.

Конфигурационные файлы проекта

Файл Info.plist содержит метаданные приложения: идентификатор пакета, версия, необходимые разрешения, поддерживаемые ориентации устройства. Современные версии Xcode позволяют использовать Swift-код для конфигурации вместо XML-формата.

// Пример конфигурации через Swift
@main
struct MyApplicationApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

Точка входа приложения

Функция main и @main атрибут

В традиционных языках программирования точкой входа служит функция main. В Swift для приложений iOS используется атрибут @main, который автоматически генерирует точку входа. Этот подход появился в Swift 5.3 и значительно упростил структуру приложения.

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
}

Атрибут @main указывает компилятору, что данный класс или структура является основной точкой входа приложения. До появления @main требовалась явная функция main в отдельном файле.

Роль класса AppDelegate

Класс AppDelegate управляет глобальными событиями приложения: запуск, переход в фоновый режим, возврат в активное состояние, завершение работы. Все методы делегата вызываются системой в определенном порядке.

class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print("Приложение начинает запуск")
return true
}

func applicationDidBecomeActive(_ application: UIApplication) {
print("Приложение стало активным")
}

func applicationWillResignActive(_ application: UIApplication) {
print("Приложение покидает активное состояние")
}

func applicationDidEnterBackground(_ application: UIApplication) {
print("Приложение перешло в фоновый режим")
}

func applicationWillEnterForeground(_ application: UIApplication) {
print("Приложение возвращается из фонового режима")
}

func applicationWillTerminate(_ application: UIApplication) {
print("Приложение завершает работу")
}
}

Жизненный цикл приложения

Состояния приложения

Приложение iOS проходит через пять основных состояний в течение своего жизненного цикла:

Not Running — приложение не запущено или было завершено системой. Это начальное состояние до первого запуска или после полного завершения работы.

Inactive — приложение работает в фоновом режиме, но не получает события. Это состояние возникает при переходе между активным и фоновым режимами, при получении уведомлений, входящих вызовов.

Active — приложение работает на переднем плане и получает все события. Это основное рабочее состояние, когда пользователь взаимодействует с интерфейсом.

Background — приложение выполняется в фоновом режиме и может выполнять ограниченные задачи. Система предоставляет несколько минут для завершения операций.

Suspended — приложение находится в фоновом режиме, но не выполняет код. Система приостанавливает приложение для экономии ресурсов.

Последовательность методов жизненного цикла

При первом запуске приложения система вызывает методы в следующем порядке:

func applicationWillFinishLaunchingWithOptions(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Инициализация до полного запуска
setupCoreServices()
return true
}

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Завершение настройки после запуска
configureAppearance()
setupNavigation()
return true
}

func applicationDidBecomeActive(_ application: UIApplication) {
// Приложение готово к взаимодействию
resumeTasks()
refreshData()
}

При переходе в фоновый режим вызываются методы:

func applicationWillResignActive(_ application: UIApplication) {
// Подготовка к переходу в фон
pauseMedia()
saveTemporaryData()
}

func applicationDidEnterBackground(_ application: UIApplication) {
// Приложение в фоновом режиме
let task = application.beginBackgroundTask {
application.endBackgroundTask(task)
}
performBackgroundTasks(task)
}

При возврате в активное состояние:

func applicationWillEnterForeground(_ application: UIApplication) {
// Подготовка к активному состоянию
restoreState()
checkForUpdates()
}

func applicationDidBecomeActive(_ application: UIApplication) {
// Полное восстановление активности
resumeTasks()
refreshInterface()
}

SceneDelegate и многозадачность

Начиная с iOS 13, Apple ввела концепцию сцен (scenes) для поддержки многозадачности на iPad и других устройствах. SceneDelegate управляет жизненным циклом отдельных окон приложения.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }

window = UIWindow(windowScene: windowScene)
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
}

func sceneDidDisconnect(_ scene: UIScene) {
// Сцена отключена от сессии
}

func sceneDidBecomeActive(_ scene: UIScene) {
// Сцена стала активной
}

func sceneWillResignActive(_ scene: UIScene) {
// Сцена покидает активное состояние
}

func sceneDidEnterBackground(_ scene: UIScene) {
// Сцена перешла в фоновый режим
}

func sceneWillEnterForeground(_ scene: UIScene) {
// Сцена возвращается из фонового режима
}
}

Жизненный цикл представлений

Состояния жизненного цикла ViewController

Каждый ViewController проходит через серию состояний при загрузке, отображении и удалении. Понимание этих состояний критически важно для правильного управления ресурсами и данными.

Loaded — представление загружено в память, но еще не отображено на экране. Метод viewDidLoad вызывается один раз при создании представления.

Appeared — представление полностью отображено и видимо пользователю. Метод viewDidAppear вызывается каждый раз при появлении представления.

Disappeared — представление скрыто, но остается в памяти. Метод viewDidDisappear вызывается при скрытии представления.

Unloaded — представление удалено из памяти. Система освобождает ресурсы при нехватке памяти или при явном удалении.

Методы жизненного цикла ViewController

class ViewController: UIViewController {

override func loadView() {
super.loadView()
print("Создание иерархии представлений")
}

override func viewDidLoad() {
super.viewDidLoad()
print("Представление загружено")
setupViews()
configureConstraints()
initializeData()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("Представление появляется")
updateContent()
prepareForDisplay()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("Представление появилось")
startAnimations()
beginDataLoading()
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("Представление исчезает")
saveState()
pauseTasks()
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("Представление исчезло")
stopAnimations()
cleanupResources()
}

override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("Подготовка к перерасчету макета")
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("Макет пересчитан")
adjustLayout()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
print("Нехватка памяти")
releaseCachedData()
}
}

Порядок вызова методов при навигации

При переходе между представлениями система вызывает методы в определенном порядке. Понимание этой последовательности помогает правильно управлять состоянием и ресурсами.

При показе нового представления:

Старый контроллер:
- viewWillDisappear
- viewDidDisappear

Новый контроллер:
- viewDidLoad
- viewWillAppear
- viewDidAppear

При возврате к предыдущему представлению:

Текущий контроллер:
- viewWillDisappear
- viewDidDisappear

Предыдущий контроллер:
- viewWillAppear
- viewDidAppear

Управление памятью в жизненном цикле

Swift использует автоматическое подсчет ссылок (ARC) для управления памятью. Понимание жизненного цикла помогает избежать утечек памяти и сильных ссылочных циклов.

class DataViewController: UIViewController {

private var dataManager: DataManager?
private var observers: [NSObjectProtocol] = []

override func viewDidLoad() {
super.viewDidLoad()
dataManager = DataManager.shared
setupObservers()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
dataManager?.startFetching()
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
dataManager?.stopFetching()
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
removeObservers()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
dataManager?.clearCache()
}

private func setupObservers() {
let notificationCenter = NotificationCenter.default

let observer1 = notificationCenter.addObserver(
forName: .dataUpdated,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleDataUpdate()
}
observers.append(observer1)

let observer2 = notificationCenter.addObserver(
forName: .connectionChanged,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleConnectionChange()
}
observers.append(observer2)
}

private func removeObservers() {
let notificationCenter = NotificationCenter.default
observers.forEach { notificationCenter.removeObserver($0) }
observers.removeAll()
}

deinit {
print("ViewController освобожден")
removeObservers()
}
}

Управление фоновыми задачами

Типы фоновых операций

iOS поддерживает несколько типов фоновых операций, каждая со своими ограничениями и временем выполнения.

Background Fetch — периодическая загрузка данных в фоновом режиме. Система определяет оптимальное время для выполнения операций.

func application(_ application: UIApplication, 
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
application.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
return true
}

func application(_ application: UIApplication,
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
fetchData { result in
switch result {
case .success:
completionHandler(.newData)
case .noData:
completionHandler(.noData)
case .failed:
completionHandler(.failed)
}
}
}

Background Processing — длительные задачи, требующие дополнительного времени. Система предоставляет до 3 минут для завершения операций.

func performBackgroundTask() {
let task = UIApplication.shared.beginBackgroundTask {
// Таймаут задачи
self.endBackgroundTask()
}

processLargeFile { success in
self.endBackgroundTask()
}
}

private func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(task)
task = UIBackgroundTaskIdentifier.invalid
}

Location Updates — отслеживание местоположения в фоновом режиме. Требует явного разрешения пользователя и постоянного отображения индикатора.

func startLocationTracking() {
locationManager.requestAlwaysAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
}

Audio Playback — воспроизведение аудио в фоновом режиме. Приложение продолжает работать, пока воспроизводится звук.

func configureAudioSession() {
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Ошибка настройки аудио сессии")
}
}

Ограничения фонового режима

Система iOS накладывает строгие ограничения на фоновые операции для экономии ресурсов и продления времени работы от батареи. Приложение получает ограниченное время для завершения операций после перехода в фоновый режим.

func applicationDidEnterBackground(_ application: UIApplication) {
print("Оставшееся время: \(application.backgroundTimeRemaining) секунд")

let task = application.beginBackgroundTask {
print("Фоновое время истекло")
self.endBackgroundTask()
}

performCriticalTasks {
application.endBackgroundTask(task)
}
}

Сборка и архивация приложения

Конфигурации сборки

Xcode поддерживает различные конфигурации сборки: Debug, Release, Custom. Каждая конфигурация имеет свои настройки оптимизации, отладки и подписи кода.

// Проверка конфигурации сборки
#if DEBUG
print("Отладочная сборка")
enableDebugFeatures()
#else
print("Релизная сборка")
enableProductionFeatures()
#endif

// Проверка режима оптимизации
#if swift(>=5.5)
print("Swift 5.5 или новее")
#endif

Схемы сборки (Schemes)

Схема определяет, как проект собирается, запускается, тестируется и профилируется. Можно создавать пользовательские схемы для разных целей: разработка, тестирование, производство.

<!-- Пример конфигурации схемы в .xcscheme -->
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = "ru"
region = "RU">
<EnvironmentVariables>
<EnvironmentVariable
key = "API_BASE_URL"
value = "https://api.development.com"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>

Архивация приложения

Архивация создает дистрибутивную версию приложения для распространения через App Store или тестирования. Процесс включает компиляцию, связывание, кодирование и создание пакета.

# Команда архивации через xcodebuild
xcodebuild archive \
-scheme "MyApplication" \
-configuration "Release" \
-archivePath "build/MyApplication.xcarchive" \
-destination "generic/platform=iOS"

# Экспорт архива
xcodebuild -exportArchive \
-archivePath "build/MyApplication.xcarchive" \
-exportPath "build/export" \
-exportOptionsPlist "ExportOptions.plist"

Распространение приложения

Подготовка к публикации

Перед публикацией в App Store необходимо подготовить метаданные: иконки, скриншоты, описание, ключевые слова, возрастной рейтинг. Все изображения должны соответствовать требованиям размера и формата.

// Пример конфигурации иконок в Assets.xcassets
{
"images": [
{
"filename": "AppIcon-20x20@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "20x20"
},
{
"filename": "AppIcon-20x20@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "20x20"
},
{
"filename": "AppIcon-60x60@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "60x60"
},
{
"filename": "AppIcon-60x60@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "60x60"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Тестирование перед релизом

Перед публикацией необходимо провести полное тестирование: функциональное, производительное, совместимости, безопасности. TestFlight позволяет распространять бета-версии среди тестировщиков.

// Проверка версии приложения
func checkAppVersion() {
let bundle = Bundle.main
let version = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
let build = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String

print("Версия: \(version ?? "неизвестно")")
print("Сборка: \(build ?? "неизвестно")")
}

// Проверка доступности функций
func checkFeatureAvailability() {
if #available(iOS 15.0, *) {
enableNewFeatures()
} else {
enableLegacyFeatures()
}
}

Отправка в App Store Connect

Процесс отправки включает загрузку архива, заполнение метаданных, выбор категории, установку цены, указание территорий распространения. После отправки приложение проходит модерацию Apple.

# Загрузка в App Store Connect через xcodebuild
xcodebuild -exportArchive \
-archivePath "build/MyApplication.xcarchive" \
-exportPath "build/export" \
-exportOptionsPlist "AppStoreExportOptions.plist" \
-allowProvisioningUpdates

# Использование Transporter для загрузки
xcrun altool --upload-app \
-f "build/export/MyApplication.ipa" \
-u "developer@apple.com" \
-p "@keychain:Application Loader"